Unique Key
contents
1. Unique Key(고유 키)란 무엇인가?
Unique Key는 관계형 데이터베이스 관리 시스템(RDBMS)의 제약 조건(Constraint) 중 하나로, 특정 열(Column)이나 열의 조합에 있는 모든 값이 서로 중복되지 않고 유일하도록 보장합니다. 주 목적은 중복 데이터가 저장되는 것을 방지하여 데이터 무결성(Data Integrity) 을 유지하는 것입니다.
- 핵심 규칙: Unique Key로 지정된 열에는 두 개의 행(Row)이 동일한 값을 가질 수 없습니다.
- 수학적 개념: 데이터셋 전체에서 해당 속성에 대해 1:1 매핑을 강제합니다.
2. 왜 사용하는가?
Primary Key(기본 키)가 행을 식별하는 주민등록번호 같은 역할을 한다면, Unique Key는 "비즈니스 키"나 보조 식별자 역할을 합니다.
- 이메일 주소:
Users테이블에서 PK는user_id(숫자)일 수 있지만,email열도 중복 가입을 막기 위해 Unique 해야 합니다. - 여권 번호: 시민 데이터베이스에서 내부 ID가 PK라도, 실제 유효성을 위해
passport_number는 고유해야 합니다. - 상품 코드(SKU): 재고 관리에서 상품 코드는 서로 겹치지 않아야 합니다.
3. Unique Key vs. Primary Key (차이점)
가장 많이 혼동되는 부분입니다. 둘 다 유일성을 보장하지만 역할이 다릅니다.
| 특징 | Primary Key (기본 키) | Unique Key (고유 키) |
|---|---|---|
| 목적 | 행을 유일하게 식별 (개체 무결성) | PK가 아닌 열의 중복 방지 (데이터 무결성) |
| NULL 값 | 허용 안 함. 절대 NULL일 수 없음. |
허용함. 대부분의 DB에서 NULL 허용 (보통 여러 개 가능). |
| 테이블당 개수 | 테이블당 오직 1개. | 테이블당 여러 개 존재 가능. |
| 인덱싱 | 자동으로 Clustered Index 생성 (보통). | 자동으로 Non-Clustered Index 생성. |
4. 작동 원리 (내부 구조)
Unique 제약 조건을 적용하면, 데이터베이스 엔진은 해당 열에 대해 Unique Index(고유 인덱스)(보통 B-Tree 구조)를 생성합니다.
- 삽입 시 확인: 새로운 행을
INSERT하려고 할 때, DB는 이 인덱스를 확인합니다. - 검증: B-Tree를 탐색하여 해당 값이 이미 존재하는지 찾습니다.
- 발견됨: "Unique Constraint Violation(고유 제약 조건 위반)" 오류를 내며
INSERT나UPDATE가 실패합니다. - 발견 안 됨: 작업이 진행되고, 새 값이 인덱스에 추가됩니다.
- 발견됨: "Unique Constraint Violation(고유 제약 조건 위반)" 오류를 내며
5. NULL 값 처리
Unique Key에서 NULL을 처리하는 방식은 DB 시스템마다 다르므로 개발자에게 중요한 디테일입니다.
- 표준 SQL / PostgreSQL / SQL Server (기본):
NULL은 "알 수 없는 값"을 의미합니다. 따라서 두 개의NULL은 서로 다른 값으로 취급됩니다.- 결과: Unique Key 열에
NULL이 포함된 행을 여러 개 넣을 수 있습니다.
- MySQL (InnoDB):
- 표준과 유사하게 여러 개의
NULL값을 허용합니다.
- 표준과 유사하게 여러 개의
- SQL Server (특이 케이스):
- 과거부터 SQL Server는 Unique Index에 오직 하나의
NULL만 허용했습니다. (여러 개를 허용하려면WHERE column IS NOT NULL조건이 있는 "Filtered Index"를 사용해야 합니다.)
- 과거부터 SQL Server는 Unique Index에 오직 하나의
6. 제약 조건의 종류
A. 단일 열 Unique Key (Single-Column)
제약 조건이 하나의 열에만 적용됩니다.
- 예시: 사용자 테이블의
email.
B. 복합 Unique Key (Composite)
제약 조건이 여러 열의 조합에 적용됩니다. 개별 열의 값은 중복될 수 있지만, 그 **조합(쌍)**은 유일해야 합니다.
- 예시: 회의실 예약 테이블 (
Booking)- A열:
room_id(회의실 번호) - B열:
booking_date(날짜) - C열:
time_slot(시간) - 제약: "1번 회의실" 예약은 많을 수 있고, "10시" 예약도 많을 수 있습니다. 하지만
room_id="1번 회의실" 이면서booking_date="2024-01-01" 이고time_slot="10시"인 데이터가 두 번 들어갈 수는 없습니다.
- A열:
7. 문법(Syntax) 예시
테이블 생성 시 Unique Key 추가:
CREATE TABLE Users (
user_id INT PRIMARY KEY,
username VARCHAR(50) NOT NULL,
email VARCHAR(100),
CONSTRAINT UQ_Email UNIQUE (email) -- 제약 조건에 이름을 붙이는 것이 좋습니다.
);
이미 존재하는 테이블에 추가:
ALTER TABLE Users
ADD CONSTRAINT UQ_Username UNIQUE (username);
복합 Unique Key 생성:
CREATE TABLE OrderDetails (
order_id INT,
product_id INT,
quantity INT,
-- 한 주문 내에서 특정 상품은 한 번만 등장하도록 제한
CONSTRAINT UQ_Order_Product UNIQUE (order_id, product_id)
);
8. Best Practices (모범 사례)
- 제약 조건 명명(Naming): 항상 제약 조건의 이름을 명시하세요 (예:
UQ_TableName_ColumnName). 시스템이 임의로 생성한 이름(예:SYS_C001234)을 사용하면 나중에 에러 로그를 보고 디버깅하기 매우 어렵습니다. - 성능(Performance): Unique Key는 인덱스를 생성합니다. 조회(
SELECT) 속도는 빨라지지만, 값을 넣을 때마다 인덱스를 검사해야 하므로INSERT와UPDATE속도는 약간 느려집니다. - Soft Delete(논리적 삭제) 이슈: 데이터를 실제로 지우지 않고
is_deleted플래그만 변경하는 방식을 쓴다면 Unique Key가 까다로워질 수 있습니다.- 문제: 사용자 A가 탈퇴(
is_deleted=1)한 후, 나중에 동일한 이메일로 다시 가입하려 하면, DB에 기존(삭제된) 데이터가 남아있어 Unique Key 위반이 발생합니다. - 해결: DB가 지원한다면 부분 인덱스(Partial Index, 예:
WHERE is_deleted = 0)를 사용해야 합니다.
- 문제: 사용자 A가 탈퇴(
다음은 부분 인덱스(Partial Index), 또는 필터링된 인덱스(Filtered Index)를 사용하여 "Soft Delete(논리적 삭제)" 충돌 문제를 해결하는 SQL 스크립트입니다.
이 방식은 PostgreSQL, SQL Server, SQLite에서 지원하는 가장 깔끔한 해결책입니다.
시나리오
Users 테이블이 있습니다. 우리는 email이 중복되지 않기를 원하지만, 오직 '활성 사용자(Active User)'끼리만 중복되지 않아야 합니다.
만약 사용자가 "논리적 삭제(is_deleted = true)" 상태라면, Unique 체크에서 무시되어야 하며, 다른 사람이 해당 이메일로 새로 가입할 수 있어야 합니다.
SQL 스크립트 (PostgreSQL / SQL Server 문법)
-- 1. 설정 (SETUP): 테이블 생성
-- 처음에는 email 컬럼에 표준 UNIQUE 제약 조건을 걸지 않습니다.
CREATE TABLE Users (
user_id INT PRIMARY KEY,
username VARCHAR(50),
email VARCHAR(100),
is_deleted BOOLEAN DEFAULT FALSE -- 0: 활성, 1: 삭제됨
);
-- 2. 해결책 (THE SOLUTION): "부분 고유 인덱스(Partial Unique Index)" 생성
-- 이 인덱스는 is_deleted가 FALSE인 행들에 대해서만 유일성을 강제합니다.
-- 삭제된 행(TRUE)은 이 인덱스에 포함되지 않으므로 검사 대상에서 제외됩니다.
CREATE UNIQUE INDEX UQ_Active_Email
ON Users (email)
WHERE is_deleted = FALSE; -- (SQL Server에서는 'is_deleted = 0' 사용)
-- ==========================================
-- 작동 확인 (DEMONSTRATION)
-- ==========================================
-- 단계 A: 새로운 유저(Bob) 추가
INSERT INTO Users (user_id, username, email, is_deleted)
VALUES (1, 'Bob', 'bob@example.com', FALSE);
-- 결과: 성공 (SUCCESS)
-- 인덱스 확인: 'bob@example.com'이 활성 상태인가? 아니요. -> 삽입 진행.
-- ------------------------------------------
-- 단계 B: 동일한 이메일로 다른 유저 추가 시도
-- INSERT INTO Users (user_id, username, email, is_deleted)
-- VALUES (2, 'FakeBob', 'bob@example.com', FALSE);
-- 결과: 실패 (FAILED)
-- 에러: 중복된 키 값이 "UQ_Active_Email" 고유 제약 조건을 위반했습니다.
-- 이유: 활성 상태(FALSE)인 행이 이미 존재하기 때문입니다.
-- ------------------------------------------
-- 단계 C: 원본 Bob을 "논리적 삭제(Soft Delete)" 처리
UPDATE Users
SET is_deleted = TRUE
WHERE user_id = 1;
-- 결과: 성공 (SUCCESS)
-- 이제 Bob은 삭제된 것으로 표시됩니다. 따라서 'UQ_Active_Email' 인덱스에서 빠지게 됩니다.
-- ------------------------------------------
-- 단계 D: Bob이 재가입하거나, 같은 이메일로 다른 사람이 가입 시도
INSERT INTO Users (user_id, username, email, is_deleted)
VALUES (3, 'NewBob', 'bob@example.com', FALSE);
-- 결과: 성공! (SUCCESS!)
-- 이유: 데이터베이스가 'bob@example.com'을 가진 활성 유저를 인덱스에서 찾습니다.
-- 기존 1번 유저는 이제 WHERE 조건(FALSE)에 맞지 않아 인덱스에서 무시됩니다.
-- 따라서 이 이메일은 "사용 가능"한 상태로 간주됩니다.
-- ==========================================
-- 최종 데이터 확인
-- ==========================================
SELECT * FROM Users;
-- 출력 결과:
-- user_id | username | email | is_deleted
-- 1 | Bob | bob@example.com | true (구 계정, 삭제됨)
-- 3 | NewBob | bob@example.com | false (신규 계정, 활성)
참고: MySQL의 경우
MySQL 8.0 이전 버전 등에서는 인덱스에 WHERE 절을 사용할 수 없습니다.
MySQL에서의 전략:
단순히 (email, deleted_at)으로 복합 Unique Key를 걸면 문제가 생깁니다. 표준 SQL에서는 NULL끼리 서로 다르다고 판단하기 때문에, deleted_at이 NULL(활성 상태)인 유저가 여러 명 생길 수 있어 유일성 보장이 깨집니다.
이를 해결하기 위해 MySQL에서는 보통 다음과 같은 방법을 씁니다:
- Generated Column(생성된 열) 사용 (MySQL 5.7+):
active_email이라는 가상 컬럼을 만들어서, 삭제된 경우NULL, 활성인 경우email값을 갖게 한 뒤, 이 컬럼에 Unique Key를 겁니다. - 애플리케이션 처리: DB 제약 조건 대신 서버 코드에서 중복을 체크합니다.
위 스크립트(부분 인덱스) 방식이 모던 데이터베이스 설계의 "모범 사례(Best Practice)"입니다.
references